package org.unidata.mdm.rest.data.service.impl;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

import org.apache.commons.lang3.ObjectUtils;
import org.unidata.mdm.core.service.MetaModelService;
import org.unidata.mdm.core.type.data.DataRecord;
import org.unidata.mdm.data.context.DeleteRelationRequestContext;
import org.unidata.mdm.data.context.DeleteRelationsRequestContext;
import org.unidata.mdm.data.context.UpsertRelationRequestContext;
import org.unidata.mdm.data.context.UpsertRelationsRequestContext;
import org.unidata.mdm.meta.configuration.Descriptors;
import org.unidata.mdm.rest.data.converter.FullRecordConverter;
import org.unidata.mdm.rest.data.converter.IntegralRecordEtalonConverter;
import org.unidata.mdm.rest.data.converter.RelationToEtalonConverter;
import org.unidata.mdm.rest.data.ro.EtalonIntegralRecordRO;
import org.unidata.mdm.rest.data.ro.EtalonRelationToRO;
import org.unidata.mdm.rest.data.ro.FullRecordRO;
import org.unidata.mdm.rest.data.ro.RelationContainsWrapperRO;
import org.unidata.mdm.rest.data.ro.RelationDeleteWrapperRO;
import org.unidata.mdm.rest.data.ro.RelationManyToManyWrapperRO;
import org.unidata.mdm.rest.data.ro.RelationReferencesWrapperRO;
import org.unidata.mdm.rest.system.service.TypedRestInputRenderer;
import org.unidata.mdm.system.context.InputCollector;
import org.unidata.mdm.system.type.pipeline.fragment.InputFragmentCollector;
import org.unidata.mdm.system.type.rendering.FieldDef;
import org.unidata.mdm.system.type.rendering.FragmentDef;
import org.unidata.mdm.system.type.rendering.MapInputSource;
import org.unidata.mdm.system.util.ConvertUtils;
import org.unidata.mdm.system.util.IdUtils;

/**
 * Upsert operation renderer
 *
 * @author Alexandr Serov
 * @since 20.09.2020
 **/
public class AtomicUpsertInputRenderer extends TypedRestInputRenderer<FullRecordRO> {

    private static final String RELATIONS_REFERENCE_PROPERTY_NAME = "relationReference";
    private static final String RELATIONS_CONTAINS_PROPERTY_NAME = "relationContains";
    private static final String RELATIONS_M2M_PROPERTY_NAME = "relationManyToMany";

    private final MetaModelService metaModelService;

    public AtomicUpsertInputRenderer(MetaModelService metaModelService) {
        super(FullRecordRO.class, FragmentDef.fragmentDef(FullRecordRO.class, "any", Arrays.asList(
            FieldDef.fieldDef(RELATIONS_CONTAINS_PROPERTY_NAME, RelationContainsWrapperRO.class),
            FieldDef.fieldDef(RELATIONS_REFERENCE_PROPERTY_NAME, RelationReferencesWrapperRO.class),
            FieldDef.fieldDef(RELATIONS_M2M_PROPERTY_NAME, RelationManyToManyWrapperRO.class)
        )));
        this.metaModelService = metaModelService;
    }

    @Override
    protected void renderFields(FragmentDef fragmentDef, InputCollector inputCollector, FullRecordRO source, MapInputSource fieldValues) {

        if (inputCollector instanceof InputFragmentCollector) {
            InputFragmentCollector<?> collector = (InputFragmentCollector<?>) inputCollector;
            // Upserts collection
            final List<UpsertRelationRequestContext> upserts = new ArrayList<>();
            final List<DeleteRelationRequestContext> deletes = new ArrayList<>();
            RelationContainsWrapperRO contains = fieldValues.get(RELATIONS_CONTAINS_PROPERTY_NAME, RelationContainsWrapperRO.class);
            RelationReferencesWrapperRO reference = fieldValues.get(RELATIONS_REFERENCE_PROPERTY_NAME, RelationReferencesWrapperRO.class);
            RelationManyToManyWrapperRO manyToMany = fieldValues.get(RELATIONS_M2M_PROPERTY_NAME, RelationManyToManyWrapperRO.class);
            if (contains != null) {
                List<EtalonIntegralRecordRO> containsUpdates = ObjectUtils.defaultIfNull(contains.getToUpdate(), Collections.emptyList());
                List<RelationDeleteWrapperRO> containsDeletes = ObjectUtils.defaultIfNull(contains.getToDelete(), Collections.emptyList());
                containsUpdates.stream().map(this::createUpsertRelationRequestContext).forEach(upserts::add);
                containsDeletes.stream().map(dro -> FullRecordConverter.convert(dro, source)).forEach(deletes::add);
            }
            if (reference != null) {
                List<EtalonRelationToRO> referencesUpdates = ObjectUtils.defaultIfNull(reference.getToUpdate(), Collections.emptyList());
                List<RelationDeleteWrapperRO> referenceDeletes = ObjectUtils.defaultIfNull(reference.getToDelete(), Collections.emptyList());
                referencesUpdates.stream().map(this::createUpsertRelationRequestContext).forEach(upserts::add);
                referenceDeletes.stream().map(dro -> FullRecordConverter.convert(dro, source)).forEach(deletes::add);
            }
            if (manyToMany != null) {
                List<EtalonRelationToRO> containsUpdates = ObjectUtils.defaultIfNull(manyToMany.getToUpdate(), Collections.emptyList());
                List<RelationDeleteWrapperRO> containsDeletes = ObjectUtils.defaultIfNull(manyToMany.getToDelete(), Collections.emptyList());
                containsUpdates.stream().map(this::createUpsertRelationRequestContext).forEach(upserts::add);
                containsDeletes.stream().map(dro -> FullRecordConverter.convert(dro, source)).forEach(deletes::add);
            }

            if (!upserts.isEmpty()) {
                collector.fragment(UpsertRelationsRequestContext.builder()
                    .relationsFrom(upserts.stream().collect(Collectors.groupingBy(UpsertRelationRequestContext::getRelationName)))
                    .build());
            }
            if (!deletes.isEmpty()) {
                collector.fragment(DeleteRelationsRequestContext.builder()
                    .relationsFrom(deletes.stream().collect(Collectors.groupingBy(DeleteRelationRequestContext::getRelationName)))
                    .build());
            }
        }
    }

    private UpsertRelationRequestContext createUpsertRelationRequestContext(EtalonIntegralRecordRO ro) {
        String toEtalonId = ro.getEtalonRecord() != null ? ro.getEtalonRecord().getEtalonId() : null;
        String toSourceSystem = toEtalonId == null ? metaModelService.instance(Descriptors.SOURCE_SYSTEMS).getAdminElement().getName() : null;
        String toExternalId = toEtalonId == null ? IdUtils.v1String() : null;
        String toEntityName = toEtalonId == null ? metaModelService.instance(Descriptors.DATA).getRelation(ro.getRelName()).getRight().getName() : null;
        Date validFrom = ro.getEtalonRecord() != null ? ConvertUtils.localDateTime2Date(ro.getEtalonRecord().getValidFrom()) : null;
        Date validTo = ro.getEtalonRecord() != null ? ConvertUtils.localDateTime2Date(ro.getEtalonRecord().getValidTo()) : null;
        DataRecord converted = IntegralRecordEtalonConverter.from(ro);
        return UpsertRelationRequestContext.builder()
            .relationEtalonKey(ro.getEtalonId())
            .etalonKey(toEtalonId)
            .sourceSystem(toSourceSystem)
            .externalId(toExternalId)
            .entityName(toEntityName)
            .record(converted)
            .relationName(ro.getRelName())
            .validFrom(validFrom)
            .validTo(validTo)
            .build();
    }

    private UpsertRelationRequestContext createUpsertRelationRequestContext(EtalonRelationToRO ro) {
        DataRecord converted = RelationToEtalonConverter.from(ro);
        return UpsertRelationRequestContext.builder()
            .etalonKey(ro.getEtalonIdTo())
            .record(converted)
            .relationName(ro.getRelName())
            .validFrom(ConvertUtils.localDateTime2Date(ro.getValidFrom()))
            .validTo(ConvertUtils.localDateTime2Date(ro.getValidTo()))
            .sourceSystem(metaModelService.instance(Descriptors.SOURCE_SYSTEMS).getAdminElement().getName())
            .build();
    }

}
